package uia.alumni.web;

import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Calendar;
import javax.persistence.EntityManager;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import uia.alumni.data.Group;
import uia.alumni.data.GroupParticipation;
import uia.alumni.data.Role;
import uia.alumni.data.User;
import uia.alumni.form.FormField;
import static uia.alumni.web.Constants.*;
import static uia.alumni.group.Constants.*;
import static uia.alumni.profile.Constants.*;

/**
 * Common superclass for commands the user can inovke. A command is a
 * servlet, but it does not have a url pattern. Instead the commands
 * are acessed through the Controller, which protects against
 * unauthorized access.
 *
 * @author Even Åby Larsen (even.larsen@uia.no)
 */
public abstract class Command extends HttpServlet {

    private final Role requiredRole;

    public Command(Role r) {
        super();
        requiredRole = r;
    }

    /** The servlet container calls init() immediately after the
     * servlet has been loaded. We use it to check that the
     * servlet name defined in web.xml is the same as the field
     * SERVLET_NAME. This allow us to use SERVLET_NAME to
     * generate URLs. Init throws an exception if the servlet
     * name is wrong, which will make the deployment fail if
     * there is a typo in web.xml.
     */
    @Override
    public void init() throws ServletException {
        ServletConfig conf = getServletConfig();
        String confName = conf.getServletName();

        try {
            Field f = getClass().getDeclaredField("SERVLET_NAME");
            if (!confName.equals(f.get(null))) {
                throw new Exception("Wrong servlet name: " + confName);
            }
        } catch (Exception e) {
            throw new ServletException("Faulty command definition in web.xml", e);
        }
    }

    /**
     * This method is called from doGet() and doPost().
     * In other words it handless both GET and POST requests.
     *
     * @param httpReq The request object.
     * @param response The response object.
     * @throws IOException if writing the response fails.
     * @throws ServletException if anything else fails and graceful recovery is impossible.
     */
    protected abstract void execute(HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException;

    /**
     * Handle get requests by calling execute()
     *
     * @param req the request object
     * @param resp the response object
     * @throws IOException if writing the response fails.
     * @throws ServletException if anything else fails and graceful recovery is impossible.
     */
    @Override
    protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        if (!isUserInRole(req, requiredRole)) {
            throw new DispatchException("User not authorized!");
        }
        execute(req, resp);
    }

    /**
     * Handle post requests by calling execute()
     *
     * @param req the request object
     * @param resp the response object
     * @throws IOException if writing the response fails.
     * @throws ServletException if anything else fails and graceful recovery is impossible.
     */
    @Override
    protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        if (!isUserInRole(req, requiredRole)) {
            throw new DispatchException("User not authorized!");
        }
        execute(req, resp);
    }

    /**
     * Store a message to the user for later display.
     * @param msg the message
     */
    protected void addMessage(HttpServletRequest req, String msg) {
        ArrayList<String> messages = (ArrayList<String>) req.getAttribute(MESSAGES);
        if (null == messages) {
            messages = new ArrayList<String>();
            req.setAttribute(MESSAGES, messages);
        }
        messages.add(msg);
    }

    /**
     * Add Form Field Errors. 
     * @param request
     * @param errors
     */
    protected void addFormErrors(HttpServletRequest request, ArrayList<FormField> errors) {
        ArrayList<FormField> err = (ArrayList<FormField>) request.getAttribute(FORMERRORS);
        if (err == null) {
            request.setAttribute(FORMERRORS, errors);
        } else {
            for (FormField f : errors) {
                err.add(f);
            }
        }
    }

    /**
     * Forward to another servlet. Forwards to home page if
     * the servlet does not exist.
     */
    protected void forwardTo(String commandName,
            HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext context = getServletContext();
        log("Dispatching to " + commandName + " from " + this.getClass().getSimpleName());
        RequestDispatcher rd = context.getNamedDispatcher(commandName);
        if (rd == null) {
            rd = context.getNamedDispatcher(Home.SERVLET_NAME);
        }
        rd.forward(request, response);
    }

    /**
     * Get the user object from the session.
     * @return the User object of the user if logged in, null otherwise.
     */
    protected User getLoggedInUser(EntityManager em, HttpServletRequest req) {
        User u = (User) req.getSession().getAttribute(USER);
        if (u != null && em != null) {
            // make sure we get any updates from the database
            u = em.merge(u);
            em.refresh(u);
        }
        return u;
    }

    /**
     * Check whether the current user has a specified role
     */
    protected boolean isUserInRole(HttpServletRequest req, Role role) {
        if (null == role) {
            return true;
        }
        User u = (User) req.getSession().getAttribute(USER);
        return (u != null && u.hasRole(role));
    }

    /**
     * Supposed to be a Generic findId Method. Used for handling
     * requests while editing specific type of info. Be careful, though. its possible
     * to fuck things up if not done right.
     *
     * Whats the point of all this? E.g.
     * Edit a user we write url: profile.edit.do?uid=501
     * This is catched by getUser method and stored in a session called
     * UID. Now this session is used to remember which user we are editing
     * between Servlets. This makes it easier to go back-and-forth when
     * handling input errors by the user, without forgetting the which user
     * it was we were editing. 
     */
    protected long findId(HttpServletRequest request, String idName) {
        if (request != null) {
            Long id = parseLong(request.getParameter(idName), -1);
            if (id == null || id < 0) { // If URL paramter comes back empty
                id = (Long) request.getSession(true).getAttribute(idName);
                if (id != null) {
                    if (DEBUG) {
                        addMessage(request, "ID found in session.");
                    }
                    return id;
                }
                if (DEBUG) {
                    addMessage(request, "ID not found in session.");
                }
            }
            if (DEBUG) {
                addMessage(request, "UD found in URL.");
            }
            request.getSession(true).setAttribute(idName, id);
            return id;
        }
        if (DEBUG) {
            addMessage(request, "em is null");
        }
        return -1;
    }

    /**
     * Used together with findId to get the User which we currently are
     * editing. The User's id we are editing is stored in a session
     * called UID.
     *
     * @return User object or null
     */
    protected User getUser(EntityManager em, HttpServletRequest request) {
        /*if (uid < 1) {
        return getLoggedInUser(em, request); // Incase something is wrong.
        }*/
        long id = findId(request, UID);
        if (em != null && id >= 0) {
            return em.find(User.class, id);
        }
        return null;
    }

    /**
     * Used together with findId to get the Group which we currently are
     * editing. The Group's id we are editing is stored in a session
     * called GID.
     * @return Group object or null
     */
    protected Group getGroup(EntityManager em, HttpServletRequest request) {
        long id = findId(request, GID);
        if (em != null && id >= 0) {
            return em.find(Group.class, id);
        }
        return null;
    }

    /* Just two helper methods to find user Role */
    protected boolean isAdmin(EntityManager em, HttpServletRequest request) {
        return getLoggedInUser(em, request).hasRole(Role.admin);
    }

    protected boolean isOwner(EntityManager em, HttpServletRequest request, User user) {
        return getLoggedInUser(em, request) == user;
    }

    protected boolean isGroupOwner(EntityManager em, HttpServletRequest request, Group group) {
        User user = getLoggedInUser(em, request);
        for (GroupParticipation gp : group.getMemberList()) {
            if (gp.getUser() == user && gp.getMemberRole() == Role.admin) {
                return true;
            }
        }
        return false;
    }
    // Utility methods for commands

    /**
     * Parse a String as an integer. If the string cannot be parsed,
     * the specified default value is returned instead.
     * @param s the String to parse.
     * @param def the default value.
     * @return the int value of s or def if s cannot be parsed as an int.
     */
    public static long parseLong(String s, long def) {
        if (s == null || s.equals("")) {
            return def;
        }
        try {
            return Long.valueOf(s);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * Get a Date object from dd.mm.yyyy
     * @param y the year
     * @param m the month
     * @param d the date
     */
    protected Date getDate(int y, int m, int d) {
        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(y, m, d);
        return new Date(cal.getTimeInMillis());
    }

    /**
     * Get a Date object from String representation of day, month and year.
     * Note: This method will fail if y,d,m don't contain String representations of ints.
     * @param yyyy the year
     * @param mm the month
     * @param dd the date
     */
    protected Date getDate(String yyyy, String mm, String dd) {
        return getDate(Integer.valueOf(yyyy), Integer.valueOf(mm), Integer.valueOf(dd));
    }
}
